时隔快两月,才产出了第二篇。在实际写博客中,发现Promise实在太难了,初稿在 2/18 就开始写,写到「链式调用」,发现自己还是对原理不够了解,于是耽搁了许久。期间又看了《你不知道的 JavaScript(中卷)》关于异步的部分,深觉自己的浅薄。
我们总是说回调不好用,因为回调地狱,但是回调也可以不写成回调地狱,只是写法问题,如:
1 | foo(function () { |
而且本质上来说,Promise也是回调啊,Promise究竟在内部做了什么事情,让我们非常乐意接受了呢?
核心机制
之前的「错误」实现,核心在于resolve里面的条件判断与两个全局变量。
更合理的实现是将需要的变量保存在对象上或者局部作用域,同时也将状态补充进来。
constructor
1 | function resolve(value) { |
只是多了_state和reject,一切看起来还好。
then 方法
1 | class Handler { |
多增加了Handler类,不将onFulfilled保存到全局globalResolved变量了,而是保存到this.handler变量上。
resolve
1 | function resolve(newValue, self) { |
然后就可以调用onFulfilled了:
1 | function resolve(newValue, self) { |
但实际执行时,handler是undefined导致报错:
1 | var fn = handler.onFulfilled; |
这就是在之前也遇到过的,resolve想通知resolved时发现resolved还没有出现的问题。
之前是通过resolve中的 4 个条件分支进行处理,而更合理的实现是增加this._deferredState变量,根据该变量进行区分,该变量有两个状态:
- 0 - 初始化时的值,表示初始化状态
- 1 - 完成同步代码的执行,等待异步代码执行完成并调用
resolve或reject
那么在什么时候改变这个状态呢?答案是then()调用时,并且也要调用一次resolve。
1 | then(onFulfilled, onRejected) { |
此时完整代码如下:
1 | function resolve(newValue, self) { |
测试用例:
1 | new FakePromise((resolve) => { |
能够成功打印hello,但是改成异步就出问题了,先打印了undefined,再打印了hello。
测试用例:1
2
3
4
5
6
7
8new FakePromise((resolve) => {
setTimeout(() => {
resolve('b');
}, 1000);
})
.then((res) => {
console.log(res); // 会先调用这里,打印 undefined,待 resolve 执行后,再次打印 b
});
所以在then内,还要增加判断是否调用了resolve,即我们之前判断是否有globalParam。
1 | function resolve(self, newValue) { |
但没有解决最核心的问题,异步。
需要将onFulfilled的调用改成在另一个任务队列中,简单的做法就是使用setTimeout包一层,变成了:
1 | setTimeout(() => { |
完成后的新代码与之前的代码做对比:
可以看到使用_state替代对value是否存在的判断;使用_deferredState替代then是否调用的判断。
上篇最后的测试用例就能够符合预期了:
1 | console.log('start'); |
结果为:start、a、c、end、b。
链式调用
下面代码会打印什么?请说出代码执行流程。
1 | // 标准 Promise |
答案是 ‘a’,第二个then获取到了开始resolve('a')的值,这是为什么呢?
就结果而言,我们可以猜测第一个then其实是返回了一个新的Promise实例,如果我们的代码要实现链式调用,会是这样的吗?
1 | then(onFulfilled, onRejected) { |
那么传什么参数呢?我们只需要一个_state === 1的promise实例,并没有要处理的逻辑。所以直接在构造函数处就可以中断了。
1 | function noop() {} |
接下来,可以认为,调用第二个then的Promise实例,「接收」了第一个then返回的值,这样才能在第二个then内拿到开始resolve的值。
那么问题来了,怎么「接收」呢?可能是在哪个步骤做这件事情呢?
而且,第一个then的函数是肯定被调用了,所以是在resolve中处理吗?
resolve
1 | function resolve(newValue, self) { |
可以看到,在then方法的最后,返回了一个新的Promise实例,调用第二个then实际上是调用的这个实例的then方法。
测试用例代码可以理解为这样的:
1 | const a1 = new FakePromise((resolve, reject) => { |
所以a2在调用then时,是也实际调用了then方法,但此时a2._state !== 1,所以没有调用。那什么时候调用呢?
由于a2._value要有值,所以肯定是在a1.then的参数调用后,即还是resolve里面。
1 | function resolve(newValue, self) { |
修改后,继续执行测试用例,得到我们想要的结果!!但是如果存在第三个then,就 GG 了。。。这是因为我们只处理了「两层」,什么意思呢,
1 | function resolve(newValue, self) { |
handler.promise.handler.onFulfilled(value);这里,其实应该拿到调用成功的值thirdValue,再执行一次:
handler.promise.handler.promise.handler.onFulfilled(thirdValue),这样有三个then也能处理,但是四个就处理不了了。。。所以我们代码思路是正确的,但是写法有问题,下面就对我们的代码进行优化,抽离重复的部分。
代码优化
将resolve拆成两个函数,resolve只负责一件事,改变promise实例状态为「成功」。
1 | function resolve(self, newValue) { |
handleResolve只负责调用回调。
1 | function handleResolve(self, handler) { |
这样无论几个then,都能够正确处理了。
但是如下测试用例又不行了。。
1 | new Promise((resolve, reject) => { |
不过回头看代码,很容易就定位到是因为在then方法内没有正确处理。
1 | class FakePromise { |
总结
OK,虽然代码还存在很多问题,不过也留在后面优化。
本篇博客,我们在之前「错误」的基础上,首先改进了then的参数不是异步调用这个问题。
但实际上
then的参数异步调用不能使用setTimeout,因为在规范中,setTimeout异步属于macroTask,而then参数的异步应该是microTask。
接下来,我们探索了「链式调用」的原理,并做了简单实现,对then的返回值有了更深的理解。
下篇预告
如果对同一个Promise实例调用两次then方法,会发生什么?
1 | const instance = new Promise((resolve, reject) => { |
会打印两次value吗,如果是这样呢?
1 | const instance = new Promise((resolve, reject) => { |
首先你预期的结果是什么,真实结果是什么,为什么?我们上面的实现能处理这种情况吗?